using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using OrbitOne.Elmah.Data;
using System.Diagnostics;

namespace OrbitOne.Elmah.Logic
{
    /// <summary>
    /// Represents the ErrorSearchFilter.
    /// </summary>
    [Serializable]
    public class ErrorSearchFilter
    {
        #region Constants
        public const string QUERY_STRING_ALL_APPLICATION = "allapp";
        public const string QUERY_STRING_APPLICATION = "app";
        public const string QUERY_STRING_TIME_FRAME = "tf";
        public const string QUERY_STRING_GROUP_VIEW = "gv";
        public const string QUERY_STRING_KEYWORD = "kw";
        public const string QUERY_STRING_ERROR_LEVEL = "el";
        public const string QUERY_STRING_ERROR_TYPE = "et";
        public const string QUERY_STRING_USER_TYPE = "ut";
        public const string QUERY_STRING_SEARCHFILTER_ID = "fsid";
        #endregion Constants

        #region Enums
        /// <summary>
        /// The result view group.
        /// </summary>
        /// <remarks>This is used to find out in what way the errors will be returned for the search filter.</remarks>
        public enum ResultViewGroup
        {
            /// <summary>Group the errors by their exception message</summary>
            ExceptionMessage = 0,
            /// <summary>Group the errors by their exception type</summary>
            ExceptionType = 1,
            /// <summary>Group the errors by their URL</summary>
            URL = 2,
            /// <summary>Doesn't group, return a list of the errors.</summary>
            ListView = 3
        }

        /// <summary>
        /// The time frame of the errors that will be returned for the search filter.
        /// </summary>
        public enum TimeFrameEnum
        {
            /// <summary>Errors of the last 12 hours</summary>
            Last12Hours = 0,
            /// <summary>Errors of the last 24 hours</summary>
            Last24Hours = 1,
            /// <summary>Errors of the last 48 hours</summary>
            Last48Hours = 2,
            /// <summary>Errors of the last week</summary>
            LastWeek = 3,
            /// <summary>Errors of the last month</summary>
            LastMonth = 4,
            /// <summary>Errors of the last year</summary>
            LastYear = 5
        }

        /// <summary>
        /// The ErrorType the errors should have that will be returned by the search filter.
        /// </summary>
        /// <remarks>This is flag</remarks>
        [Flags]
        public enum ErrorTypeEnum
        {
            /// <summary>Errors of the type infrastructure </summary>
            Infrastructure = 1,
            /// <summary>Errors of the type code </summary>
            Code = 2,
            /// <summary>Errors of any of the ErrorTypes </summary>
            All = Infrastructure | Code
        }

        /// <summary>
        /// The User type the errors should be off that will be returned by the search filter.
        /// </summary>
        /// <remarks>This is flag</remarks>
        [Flags]
        public enum UserTypeEnum
        {
            /// <summary>Errors of anonymous users</summary>
            Anonymous = 1,
            /// <summary>Errors of authenticated users</summary>
            User = 2,
            /// <summary>Errors generated by a Bot (spider, crawler, ...)</summary>
            Webbots = 4,
            /// <summary>Errors generated by the Home User</summary>
            HomeUser = 8,
            /// <summary>Errors of any of the UserTypes</summary>
            All = Anonymous | User | Webbots | HomeUser
        }

        /// <summary>
        /// The error level an error should have that will be returned by the search filter.
        /// </summary>
        /// <remarks>This is flag</remarks>
        [Flags]
        public enum ErrorLevelEnum
        {
            /// <summary>Errors with level 0 or 1</summary>
            Level1 = 1,
            /// <summary>Errors with level 2 or 3</summary>
            Level2 = 2,
            /// <summary>Errors with level 4 or 5</summary>
            Level3 = 4,
            /// <summary>Errors with level between 0 and 5</summary>
            All = Level1 | Level2 | Level3
        }
        #endregion

        #region Private Members
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly IList<string> _applications = new List<string>();
        #endregion
  
        #region Properties
        /// <summary>
        /// 
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// Gets the applications of the search filter.
        /// </summary>
        public IList<string> Applications { get { return _applications; } }
        /// <summary>
        /// Gets the <code>UserType</code> of the search filter.
        /// </summary>
        public UserTypeEnum UserType { get; set; }
        /// <summary>
        /// Gets the <code>ErrorLevel</code> of the search filter.
        /// </summary>
        public ErrorLevelEnum ErrorLevel { get; set; }
        /// <summary>
        /// Gets the <code>ErrorType</code> of the search filter.
        /// </summary>
        public ErrorTypeEnum ErrorType { get; set; }
        /// <summary>
        /// Gets the <code>TimeFrame</code> of the search filter.
        /// </summary>
        public TimeFrameEnum TimeFrame { get; set; }
        /// <summary>
        /// Gets the <code>ResultViewGroup</code> of the search filter.
        /// </summary>
        public ResultViewGroup ViewGroup { get; set; }
        /// <summary>
        /// Gets the keyword of the search filter.
        /// </summary>
        public string Keyword { get; set; }
        /// <summary>
        /// Gets wether all the applications should be searched by the search filter.
        /// </summary>
        public bool AllApplications { get; set; }
        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new search filter.
        /// </summary>
        /// <param name="applications">The applications on the search filter (if null all applications will be searched)</param>
        /// <param name="timeFrame"></param>
        /// <param name="keyword"></param>
        /// <param name="errorType"></param>
        /// <param name="errorLevel"></param>
        /// <param name="userType"></param>
        /// <param name="viewGroup"></param>
        /// <remarks>if applications == null then all applictions will be true.</remarks>
        public ErrorSearchFilter(IList<string> applications, TimeFrameEnum timeFrame, string keyword, ErrorTypeEnum errorType,
                                 ErrorLevelEnum errorLevel, UserTypeEnum userType, ResultViewGroup viewGroup)
        {
            if (applications != null && applications.Count != 0)
            {
                _applications = applications;
            }
            else
            {
                AllApplications = true;
            }
            this.TimeFrame = timeFrame;
            this.Keyword = keyword;
            this.ErrorType = errorType;
            this.ErrorLevel = errorLevel;
            this.UserType = userType;
            this.ViewGroup = viewGroup;
        }
        /// <summary>
        /// Creates a new search filter with the default values.
        /// </summary>
        /// <remarks>default values:
        /// - Timeframe: Last week,
        /// - Errortype: All
        /// - Errorlevel: All
        /// - UserType: All
        /// - ResultView: Exception message
        /// - All Applications</remarks>
        public ErrorSearchFilter()
        {
            TimeFrame = TimeFrameEnum.Last24Hours;
            ErrorType = ErrorTypeEnum.All;
            ErrorLevel = ErrorLevelEnum.All;
            UserType = UserTypeEnum.All;
            ViewGroup = ResultViewGroup.ListView;
            AllApplications = true;
        }
        #endregion

        #region Internal Methods
        /// <summary>
        /// Gets the result of the search filter based on the errors specified.
        /// </summary>
        /// <param name="errors">The errors to search</param>
        /// <returns>An <code>IEnumerable:SearchRecord</code> that are the result of this search filter.</returns>
        internal SearchRecordCollection GetSearchView(IQueryable<ELMAH_Error> errors, int startRowIndex, int maximumRows)
        {
            return GetSearchView(errors, DateTime.Now.AddYears(-1), startRowIndex, maximumRows);
        }

        /// <summary>
        /// Gets the result of the search filter based on the errors specified.
        /// </summary>
        /// <param name="errors">The errors to search</param>
        /// <param name="end">The end of the Search Filter's time frame.</param>
        /// <returns>An <code>IEnumerable:SearchRecord</code> that are the result of this search filter.</returns>
        internal SearchRecordCollection GetSearchView(IQueryable<ELMAH_Error> errors, DateTime? end, int startRowIndex, int maximumRows)
        {
            var sfv = new SearchViewFactory(GetWherePredicate(errors, end));
            return sfv.CreateSearchView(ViewGroup, startRowIndex, maximumRows);
        }
        #endregion

        #region Public Methods
        public string ToQueryString(ResultViewGroup viewGroup)
        {
            var sb = new StringBuilder();
            sb.AppendFormat("{0}={1}", QUERY_STRING_ALL_APPLICATION, AllApplications.ToString().ToLower());
            if (!AllApplications)
                sb.AppendFormat("&{0}={1}", QUERY_STRING_APPLICATION, string.Join(",", Applications.ToArray()));
            sb.AppendFormat("&{0}={1}", QUERY_STRING_ERROR_LEVEL, (int)ErrorLevel);
            sb.AppendFormat("&{0}={1}", QUERY_STRING_ERROR_TYPE, (int)ErrorType);
            sb.AppendFormat("&{0}={1}", QUERY_STRING_GROUP_VIEW, (int)viewGroup);
            sb.AppendFormat("&{0}={1}", QUERY_STRING_TIME_FRAME, (int)TimeFrame);
            sb.AppendFormat("&{0}={1}", QUERY_STRING_USER_TYPE, (int)UserType);
            sb.AppendFormat("&{0}={1}", QUERY_STRING_KEYWORD, HttpUtility.UrlEncode(Keyword));
            return sb.ToString();
        }
        #endregion

        #region Private Methods
        private IQueryable<ELMAH_Error> GetWherePredicate(IQueryable<ELMAH_Error> errorEntities, DateTime? end)
        {
            var s = errorEntities.Where(GetTimeFrameWhere(end))
                .Where(GetErrorLevelWhere())
                .Where(GetErrorTypeWhere())
                .Where(GetUserTypeWhere())
                .Where(GetKeywordWhere());

            if (!AllApplications)
                s = s.Where(GetApplicationsWhere());

            return s;
        }

        private Expression<Func<ELMAH_Error, bool>> GetApplicationsWhere()
        {
            var predicate = PredicateBuilder.False<ELMAH_Error>();
            foreach (var application in Applications)
            {
                var app = application;
                predicate = predicate.Or(error => error.ApplicationName == app);
            }
            return predicate;
        }

        private Expression<Func<ELMAH_Error, bool>> GetErrorTypeWhere()
        {
            var predicate = PredicateBuilder.False<ELMAH_Error>();
            if ((ErrorType & ErrorTypeEnum.Code) == ErrorTypeEnum.Code)
            {
                predicate = predicate.Or(error => error.ErrorType == ErrorTypeEnum.Code.ToString());
            }
            if ((ErrorType & ErrorTypeEnum.Infrastructure) == ErrorTypeEnum.Infrastructure)
            {
                predicate = predicate.Or(error => error.ErrorType == ErrorTypeEnum.Infrastructure.ToString());
            }
            if (predicate == PredicateBuilder.False<ELMAH_Error>())
            {
                predicate = PredicateBuilder.True<ELMAH_Error>();
            }
            return predicate;
        }

        private Expression<Func<ELMAH_Error, bool>> GetUserTypeWhere()
        {
            var predicate = PredicateBuilder.False<ELMAH_Error>();
            if ((UserType & UserTypeEnum.Anonymous) == UserTypeEnum.Anonymous)
            {
                predicate = predicate.Or(error => error.UserName == "");
            }
            if ((UserType & UserTypeEnum.HomeUser) == UserTypeEnum.HomeUser)
            {
                predicate = predicate.Or(error => error.AppliedRules.Contains(Constants.RuleInternalUser));
            }
            if ((UserType & UserTypeEnum.User) == UserTypeEnum.User)
            {
                predicate = predicate.Or(error => error.UserName != "");
            }
            if ((UserType & UserTypeEnum.Webbots) == UserTypeEnum.Webbots)
            {
                predicate = predicate.Or(error => error.AppliedRules.Contains(Constants.RuleNameBots));
            }
            if (predicate == PredicateBuilder.False<ELMAH_Error>())
            {
                predicate = PredicateBuilder.True<ELMAH_Error>();
            }
            return predicate;
        }

        private Expression<Func<ELMAH_Error, bool>> GetErrorLevelWhere()
        {
            var predicate = PredicateBuilder.False<ELMAH_Error>();
            if ((ErrorLevel & ErrorLevelEnum.Level1) == ErrorLevelEnum.Level1)
            {
                predicate = predicate.Or(error => error.ErrorLevel <= 1);
            }
            if ((ErrorLevel & ErrorLevelEnum.Level2) == ErrorLevelEnum.Level2)
            {
                predicate = predicate.Or(error => error.ErrorLevel == 2 || error.ErrorLevel == 3);
            }
            if ((ErrorLevel & ErrorLevelEnum.Level3) == ErrorLevelEnum.Level3)
            {
                predicate = predicate.Or(error => error.ErrorLevel == 4 || error.ErrorLevel == 5);
            }
            if (predicate == PredicateBuilder.False<ELMAH_Error>())
            {
                predicate = PredicateBuilder.True<ELMAH_Error>();
            }
            return predicate;
        }

        private Expression<Func<ELMAH_Error, bool>> GetTimeFrameWhere(DateTime? end)
        {
            if (!end.HasValue)
            {
                end = DateTime.Now;
            }
            DateTime start;
            switch (TimeFrame)
            {
                case TimeFrameEnum.Last12Hours:
                    start = end.Value.AddHours(-12);
                    break;
                case TimeFrameEnum.Last24Hours:
                    start = end.Value.AddDays(-1);
                    break;
                case TimeFrameEnum.Last48Hours:
                    start = end.Value.AddDays(-2);
                    break;
                case TimeFrameEnum.LastMonth:
                    start = end.Value.AddMonths(-1);
                    break;
                case TimeFrameEnum.LastWeek:
                    start = end.Value.AddDays(-7);
                    break;
                default:
                    start = end.Value.AddYears(-1);
                    break;
            }

            return error => error.HappeningTime > start &&
                            error.HappeningTime <= end;
        }

        private Expression<Func<ELMAH_Error, bool>> GetKeywordWhere()
        {
            return (!string.IsNullOrEmpty(Keyword)
                        ? PredicateBuilder.True<ELMAH_Error>()
                              .And(error => error.ErrorMessage.Replace("\r","").Replace("\n","").Contains(Keyword)
                                            || Keyword.Replace("{","").Replace("}","").ToLower() == error.ErrorId.ToString().ToLower()
                                            || error.HostName == Keyword 
                                            || error.ExceptionType.Contains(Keyword)
                                            || (((string)error.AllXml).Contains(Keyword.Substring(Keyword.IndexOf("/") >=0 ? Keyword.IndexOf("/") + 1 : 0)) 
                                                && (((string)error.AllXml).Contains(Keyword.Substring(0, Keyword.IndexOf("/") >=0 ? Keyword.IndexOf("/") : 0)))))
                        : PredicateBuilder.True<ELMAH_Error>());
        }
        #endregion
    }
}